/**
 * Copyright (c) 2016-2023, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jpress.web.admin;

import com.alibaba.fastjson.JSONObject;
import com.jfinal.aop.Clear;
import com.jfinal.aop.Inject;
import com.jfinal.core.ActionKey;
import com.jfinal.kit.LogKit;
import com.jfinal.plugin.activerecord.Db;
import io.jboot.utils.CookieUtil;
import io.jboot.utils.HttpUtil;
import io.jboot.utils.StrUtil;
import io.jboot.web.controller.annotation.RequestMapping;
import io.jpress.JPressConsts;
import io.jpress.JPressOptions;
import io.jpress.commons.utils.SessionUtils;
import io.jpress.model.Department;
import io.jpress.model.Role;
import io.jpress.model.User;
import io.jpress.service.DepartmentService;
import io.jpress.service.RoleService;
import io.jpress.service.UserOpenidService;
import io.jpress.service.UserService;
import io.jpress.web.base.AdminControllerBase;

import java.util.*;

/**
 * @author Michael Yang 杨福海 （fuhai999@gmail.com）
 * @version V1.0
 */
@RequestMapping(value = "/admin/oauth2", viewPath = JPressConsts.DEFAULT_ADMIN_VIEW)
public class _Oauth2Controller extends AdminControllerBase {

    @Inject
    private UserService userService;

    @Inject
    private RoleService roleService;

    @Inject
    private DepartmentService departmentService;

    @Inject
    private UserOpenidService userOpenidService;

    private static final String CALLBACK_ACTION = "/admin/oauth2/callback";

    @Clear
    public void login() {
        boolean oauth2LoginEnable = JPressOptions.getAsBool("admin_login_oauth2_enable", false);
        if (!oauth2LoginEnable) {
            render404Error();
            return;
        }

        String clientAppId = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_appid"));
        String clientAppSecret = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_secret"));
        String authorizeUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_authorize_url"));
        String tokenUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_token_url"));
        String profileUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_profile_url"));

        if (StrUtil.isAnyBlank(clientAppId, clientAppSecret, authorizeUrl, tokenUrl, profileUrl)) {
            renderText("第三方登录授权信息不完善，请联系管理员。");
            return;
        }

        String redirect_uri = JPressOptions.get(JPressConsts.OPTION_WEB_DOMAIN);
        redirect_uri = redirect_uri + CALLBACK_ACTION;

        Map<String, Object> context = new HashMap<>();
        context.put("client_id", clientAppId);
        context.put("client_secret", clientAppSecret);
        context.put("redirect_uri", StrUtil.urlEncode(redirect_uri));
        context.put("timestamp", System.currentTimeMillis());
        context.put("state", StrUtil.uuid());

        String realAuthorizeUrl = replaceParas(authorizeUrl, context);
        redirect(realAuthorizeUrl);
    }


    private String replaceParas(String authorizeUrl, Map<String, Object> context) {
        for (Map.Entry<String, Object> stringObjectEntry : context.entrySet()) {
            authorizeUrl = authorizeUrl.replace("{" + stringObjectEntry.getKey().trim() + "}", String.valueOf(stringObjectEntry.getValue()));
        }
        return authorizeUrl;
    }


    @Clear()
    @ActionKey(CALLBACK_ACTION)
    public void callback() {

        String clientAppId = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_appid"));
        String clientAppSecret = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_secret"));
//        String authorizeUrl = JPressOptions.get("admin_login_oauth2_authorize_url");
        String tokenUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_token_url"));
        String profileUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_profile_url"));
        String defaultDeptId = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_default_dept_id"));
        String defaultRoleId = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_default_role_id"));
        String successUrl = StrUtil.unEscapeHtml(JPressOptions.get("admin_login_oauth2_success_url", "/admin/index"));


        String uid = CookieUtil.get(this, JPressConsts.COOKIE_UID);
        if (StrUtil.isNotBlank(uid) && SessionUtils.isLoginedOk(Long.valueOf(uid))) {
            redirect(successUrl);
            return;
        }


        String code = getPara("code");

        // 不传入 code 的情况下，
        // 可能是 oauth 平台直接点击了应用
        // 跳转到登录页面
        if (StrUtil.isBlank(code)) {
            redirect("/admin/oauth2/login");
            return;
        }


        String redirect_uri = JPressOptions.get(JPressConsts.OPTION_WEB_DOMAIN);
        redirect_uri = redirect_uri + CALLBACK_ACTION;

        Map<String, Object> context = new HashMap<>();
        context.putAll(getParas());

        context.put("client_id", clientAppId);
        context.put("client_secret", clientAppSecret);
        context.put("redirect_uri", StrUtil.urlEncode(redirect_uri));
        context.put("timestamp", System.currentTimeMillis());
        context.put("state", StrUtil.uuid());
        context.put("code", code);

        String realTokenUrlWithParas = replaceParas(tokenUrl, context);
        //说明 realProfileUrl 还有参数未被替换，可能是 token 返回的内容是错误的
        if (realTokenUrlWithParas.contains("{") && realTokenUrlWithParas.contains("}")) {
            LogKit.error("oauth2 获取 Token 信息的 url 未被替换参数：{}", realTokenUrlWithParas);
            renderText("登录出错，请联系管理员。错误信息：Access Token Url 参数未正确设置。");
            return;
        }

//        String realTokenUrl = realTokenUrlWithParas.substring(0, realTokenUrlWithParas.indexOf("?"));
//        Map queryParas = StrUtil.queryStringToMap(realTokenUrlWithParas.substring(realTokenUrlWithParas.indexOf("?") + 1));

        //获取 token 一般都是 post 提交
        String tokenResp = HttpUtil.httpPost(realTokenUrlWithParas);
        if (StrUtil.isBlank(tokenResp)) {
            LogKit.error("oauth2 登录无法获取第三方 Token 信息，请检查该 URL 是否可以正常访问：{}", realTokenUrlWithParas);
            renderText("登录出错，请联系管理员。错误信息：无法获取用户中心的 Access Token 信息。");
            return;
        }

        JSONObject tokenMap = null;
        try {
            tokenMap = JSONObject.parseObject(tokenResp);
        } catch (Exception e) {
            LogKit.error(e.toString(), e);
        }

        if (tokenMap == null) {
            LogKit.error("oauth2 登录无法获取第三方 Token 信息，后台返回内容：{}", tokenResp);
            renderText("登录出错，请联系管理员。错误信息：无法解析 Access Token 信息。");
            return;
        }

        context.putAll(tokenMap);

        String realProfileUrl = replaceParas(profileUrl, context);

        //说明 realProfileUrl 还有参数未被替换，可能是 token 返回的内容是错误的
        if (realProfileUrl.contains("{") && realProfileUrl.contains("}")) {
            LogKit.error("oauth2 获取用户信息的 url 未被替换参数：{}", realProfileUrl);
            renderText("登录出错，请联系管理员。错误信息：获取用户信息 Url 参数未正确设置。");
            return;
        }

        String profileResp = HttpUtil.httpGet(realProfileUrl);
        if (StrUtil.isBlank(tokenResp)) {
            LogKit.error("后台 oauth2 登录无法获取第三方用户信息，请检查该 URL 是否可以正常访问：{}", realProfileUrl);
            renderText("登录出错，请联系管理员。错误信息：无法获取用户信息。");
            return;
        }


        JSONObject profileMap = null;
        try {
            profileMap = JSONObject.parseObject(profileResp);
        } catch (Exception e) {
            LogKit.error(e.toString(), e);
        }

        if (profileMap == null || profileMap.isEmpty()) {
            LogKit.error("后台 oauth2 登录无法获取第三方 Token 信息，后台返回内容：{}", tokenResp);
            renderText("登录出错，请联系管理员。错误信息：用户信息未能正确解析。");
            return;
        }


        String oauthUserId = getStringByAnyKey(profileMap, "unionid", "openid", "id");
        if (StrUtil.isBlank(oauthUserId)) {
            LogKit.error("后台 oauth2 登录无法获取第三方用户ID，返回的 JSON 内容为：{}", profileMap);
            renderText("登录出错，请联系管理员。错误信息：无法获取用户 ID。");
            return;
        }

        String nickname = getStringByAnyKey(profileMap, "nickname", "nick_name", "nick", "name", "username", "user_name", "user", "screen_name");
        if (StrUtil.isBlank(nickname)) {
            nickname = "未设置昵称";
        }

        String avatar = getStringByAnyKey(profileMap, "avatar", "avatar_url", "avatar_large", "headimgurl");

        User loginUser = userOpenidService.findByTypeAndOpenId(clientAppId, oauthUserId);

        //用户存在，登录成功
        if (loginUser != null) {
            SessionUtils.record(loginUser.getId());
            CookieUtil.put(this, JPressConsts.COOKIE_UID, loginUser.getId());
            redirect(successUrl);
            return;
        }

        final Department department = departmentService.findById(Long.valueOf(defaultDeptId));
        if (department == null) {
            LogKit.error("后台配置的第三方登录默认部门不存在！！！需重新配置。");
            renderText("登录出错，请联系管理员。错误信息：后台配置的部门信息不存在。");
            return;
        }


        final Role role = roleService.findById(Long.valueOf(defaultRoleId));
        if (role == null) {
            LogKit.error("后台配置的第三方登录默认角色不存在！！！需重新配置。");
            renderText("登录出错，请联系管理员。错误信息：后台配置的角色不存在。");
            return;
        }

        // 用户不存在
        User newUser = new User();
        newUser.setNickname(nickname);
        newUser.setAvatar(avatar);
        newUser.setCreated(new Date());
        newUser.setCreateSource(User.SOURCE_ADMIN_OAUTH2);
        newUser.setStatus(User.STATUS_OK);

        boolean success = Db.tx(() -> {
            Long newUserId = (Long) userService.save(newUser);
            if (newUserId == null) {
                return false;
            }

            userService.updateDepartmentId(newUserId, department.getId());

            if (!roleService.doResetUserRoles(newUserId, role.getId())) {
                return false;
            }

            if (!userOpenidService.saveOrUpdate(newUserId, clientAppId, oauthUserId)) {
                return false;
            }
            return true;
        });


        if (success) {
            SessionUtils.record(newUser.getId());
            CookieUtil.put(this, JPressConsts.COOKIE_UID, newUser.getId());
            redirect(successUrl);
        } else {
            LogKit.error("oauth2 保存用户数据出错！！！。");
            renderText("登录出错，请联系管理员。错误信息：无法保存用户数据。");
        }

    }


    private String getStringByAnyKey(JSONObject profileMap, String... keys) {
        List<JSONObject> childs = new ArrayList<>();
        for (Map.Entry<String, Object> entry : profileMap.entrySet()) {
            if (entry.getValue() instanceof JSONObject) {
                childs.add((JSONObject) entry.getValue());
            } else {
                for (String key : keys) {
                    if (entry.getKey().equals(key)) {
                        return (String) entry.getValue();
                    }
                }
            }
        }

        for (JSONObject child : childs) {
            String retValue = getStringByAnyKey(child, keys);
            if (StrUtil.isNotBlank(retValue)) {
                return retValue;
            }
        }

        return null;
    }

}
